<?php
/*
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

namespace Doctrine\ORM\Tools;

use Doctrine\DBAL\Schema\Index;

use Doctrine\ORM\EntityRepository;

use Doctrine\ORM\Mapping\ClassMetadataInfo, Doctrine\ORM\Mapping\AssociationMapping, Doctrine\Common\Util\Inflector;

/**
 * Generic class used to generate PHP5 entity classes from ClassMetadataInfo instances
 *
 * [php]
 * $classes = $em->getClassMetadataFactory()->getAllMetadata();
 *
 * $generator = new \Doctrine\ORM\Tools\EntityGenerator();
 * $generator->setGenerateAnnotations(true);
 * $generator->setGenerateStubMethods(true);
 * $generator->setRegenerateEntityIfExists(false);
 * $generator->setUpdateEntityIfExists(true);
 * $generator->generate($classes, '/path/to/generate/entities');
 *
 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link    www.doctrine-project.org
 * @since   2.0
 * @version $Revision$
 * @author  Benjamin Eberlei <kontakt@beberlei.de>
 * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author  Jonathan Wage <jonwage@gmail.com>
 * @author  Roman Borschel <roman@code-factory.org>
 */
class EntityGenerator {
	/**
	 * @var bool
	 */
	private $_backupExisting = true;
	
	/** The extension to use for written php files */
	private $_extension = '.php';
	
	/** Whether or not the current ClassMetadataInfo instance is new or old */
	private $_isNew = true;
	
	private $_staticReflection = array();
	
	/** Number of spaces to use for indention in generated code */
	private $_numSpaces = 4;
	
	/** The actual spaces to use for indention */
	private $_spaces = '    ';
	
	/** The class all generated entities should extend */
	private $_classToExtend;
	
	/** Whether or not to generation annotations */
	private $_generateAnnotations = false;
	
	/**
	 * @var string
	 */
	private $_annotationsPrefix = '';
	
	/** Whether or not to generated sub methods */
	private $_generateEntityStubMethods = false;
	
	/** Whether or not to update the entity class if it exists already */
	private $_updateEntityIfExists = false;
	
	/** Whether or not to re-generate entity class if it exists already */
	private $_regenerateEntityIfExists = false;
	
	/** Repository directory where are placed all repository with name like UserRepository */
	private $_repositoryDirectory = null;
	
	private static $_classTemplate = '<?php
<namespace>

<entityAnnotation>
<entityClassName>{
<entityBody>
}';
	
	private static $_getMethodTemplate = '/** <description> @return <variableType>$<variableName> */
public function <methodName>(){ return $this-><fieldName>; }';
	
	private static $_setMethodTemplate = '/** <description> @param <variableType>$<variableName> */
public function <methodName>(<methodTypeHint>$<variableName>){ $this-><fieldName> = $<variableName>; }';
	
	private static $_addMethodTemplate = '/**
 * <description>
 *
 * @param <variableType>$<variableName>
 */
public function <methodName>(<methodTypeHint>$<variableName>){
<spaces>$this-><fieldName>[] = $<variableName>;
}';
	
	private static $_lifecycleCallbackMethodTemplate = '/**
 * @<name>
 */
public function <methodName>(){
<spaces>// Add your code here
}';
	
	private static $_constructorMethodTemplate = 'public function __construct(){
<defaultValues>
<spaces><collections>
}
';
	
	/**
	 * Generate and write entity classes for the given array of ClassMetadataInfo instances
	 *
	 * @param array $metadatas
	 * @param string $outputDirectory
	 * @return void
	 */
	public function generate(array $metadatas, $outputDirectory) {
		foreach( $metadatas as $metadata ) {
			$this->writeEntityClass( $metadata, $outputDirectory );
		}
	}
	
	/**
	 * Generated and write entity class to disk for the given ClassMetadataInfo instance
	 *
	 * @param ClassMetadataInfo $metadata
	 * @param string $outputDirectory
	 * @return void
	 */
	public function writeEntityClass(ClassMetadataInfo $metadata, $outputDirectory) {
		$path = $outputDirectory . '/' . str_replace( '\\', DIRECTORY_SEPARATOR, $metadata->name ) . $this->_extension;
		$dir = dirname( $path );
		
		if( !is_dir( $dir ) ) {
			mkdir( $dir, 0777, true );
		}
		
		$backupDir = dirname( $path ) . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
		if( !is_dir( $backupDir ) ) {
			mkdir( $backupDir, 0777, true );
		}
		
		$this->_isNew = !file_exists( $path ) || (file_exists( $path ) && $this->_regenerateEntityIfExists);
		
		if( !$this->_isNew ) {
			$this->_parseTokensInEntityFile( file_get_contents( $path ) );
		}
		
		if( $this->_backupExisting && file_exists( $path ) ) {
			$backupPath = $backupDir . basename( $path ) . "~";
			if( !copy( $path, $backupPath ) ) {
				throw new \RuntimeException( "Attempt to backup overwritten entitiy file but copy operation failed." );
			}
		}
		
		// If entity doesn't exist or we're re-generating the entities entirely
		if( $this->_isNew ) {
			file_put_contents( $path, $this->generateEntityClass( $metadata ) );
		
		// If entity exists and we're allowed to update the entity class
		} else if( !$this->_isNew && $this->_updateEntityIfExists ) {
			file_put_contents( $path, $this->generateUpdatedEntityClass( $metadata, $path ) );
		}
	}
	
	/**
	 * Generate a PHP5 Doctrine 2 entity class from the given ClassMetadataInfo instance
	 *
	 * @param ClassMetadataInfo $metadata
	 * @return string $code
	 */
	public function generateEntityClass(ClassMetadataInfo $metadata) {
		$placeHolders = array( '<namespace>', '<entityAnnotation>', '<entityClassName>', '<entityBody>' );
		
		$replacements = array( $this->_generateEntityNamespace( $metadata ), $this->_generateEntityDocBlock( $metadata ), $this->_generateEntityClassName( $metadata ), $this->_generateEntityBody( $metadata ) );
		
		$code = str_replace( $placeHolders, $replacements, self::$_classTemplate );
		return str_replace( '<spaces>', $this->_spaces, $code );
	}
	
	/**
	 * Generate the updated code for the given ClassMetadataInfo and entity at path
	 *
	 * @param ClassMetadataInfo $metadata
	 * @param string $path
	 * @return string $code;
	 */
	public function generateUpdatedEntityClass(ClassMetadataInfo $metadata, $path) {
		$currentCode = file_get_contents( $path );
		
		$body = $this->_generateEntityBody( $metadata );
		$body = str_replace( '<spaces>', $this->_spaces, $body );
		$last = strrpos( $currentCode, '}' );
		
		return substr( $currentCode, 0, $last ) . $body . (strlen( $body ) > 0 ? "\n" : '') . "}";
	}
	
	/**
	 * Set the number of spaces the exported class should have
	 *
	 * @param integer $numSpaces
	 * @return void
	 */
	public function setNumSpaces($numSpaces) {
		$this->_spaces = str_repeat( ' ', $numSpaces );
		$this->_numSpaces = $numSpaces;
	}
	
	/**
	 * Set the extension to use when writing php files to disk
	 *
	 * @param string $extension
	 * @return void
	 */
	public function setExtension($extension) {
		$this->_extension = $extension;
	}
	
	/**
	 * Set the name of the class the generated classes should extend from
	 *
	 * @return void
	 */
	public function setClassToExtend($classToExtend) {
		$this->_classToExtend = $classToExtend;
	}
	
	/**
	 * Set whether or not to generate annotations for the entity
	 *
	 * @param bool $bool
	 * @return void
	 */
	public function setGenerateAnnotations($bool) {
		$this->_generateAnnotations = $bool;
	}
	
	/**
	 * Set an annotation prefix.
	 *
	 * @param string $prefix
	 */
	public function setAnnotationPrefix($prefix) {
		$this->_annotationsPrefix = $prefix;
	}
	
	/**
	 * Set whether or not to try and update the entity if it already exists
	 *
	 * @param bool $bool
	 * @return void
	 */
	public function setUpdateEntityIfExists($bool) {
		$this->_updateEntityIfExists = $bool;
	}
	
	/**
	 * Set whether or not to regenerate the entity if it exists
	 *
	 * @param bool $bool
	 * @return void
	 */
	public function setRegenerateEntityIfExists($bool) {
		$this->_regenerateEntityIfExists = $bool;
	}
	
	/**
	 * Set whether or not to generate stub methods for the entity
	 *
	 * @param bool $bool
	 * @return void
	 */
	public function setGenerateStubMethods($bool) {
		$this->_generateEntityStubMethods = $bool;
	}
	
	/**
	 * Should an existing entity be backed up if it already exists?
	 */
	public function setBackupExisting($bool) {
		$this->_backupExisting = $bool;
	}
	
	/** 
	 * The repository folder will be used to find information on the different repositories
	 * Allow to have separate repository from model, for better generation 
	 */
	public function setRepositoryFolder($dirName) {
		$this->_repositoryDirectory = $dirName;
	}
	
	private function _generateEntityNamespace(ClassMetadataInfo $metadata) {
		if( $this->_hasNamespace( $metadata ) ) {
			return 'namespace ' . $this->_getNamespace( $metadata ) . ';';
		}
	}
	
	private function _generateEntityClassName(ClassMetadataInfo $metadata) {
		return 'class ' . $this->_getClassName( $metadata ) . ($this->_extendsClass() ? ' extends ' . $this->_getClassToExtendName() : null);
	}
	
	private function _generateEntityBody(ClassMetadataInfo $metadata) {
		$fieldMappingProperties = $this->_generateEntityFieldMappingProperties( $metadata );
		$associationMappingProperties = $this->_generateEntityAssociationMappingProperties( $metadata );
		$stubMethods = $this->_generateEntityStubMethods ? $this->_generateEntityStubMethods( $metadata ) : null;
		$lifecycleCallbackMethods = $this->_generateEntityLifecycleCallbackMethods( $metadata );
		
		$code = array();
		
		if( $fieldMappingProperties ) {
			$code[] = $fieldMappingProperties;
		}
		
		if( $associationMappingProperties ) {
			$code[] = $associationMappingProperties;
		}
		
		$code[] = $this->_generateEntityConstructor( $metadata );
		
		if( $stubMethods ) {
			$code[] = $stubMethods;
		}
		
		if( $lifecycleCallbackMethods ) {
			$code[] = $lifecycleCallbackMethods;
		}
		
		return implode( "\n", $code );
	}
	
	private function _generateEntityConstructor(ClassMetadataInfo $metadata) {
		if( $this->_hasMethod( '__construct', $metadata ) ) {
			return '';
		}
		
		$metadata;
		
		$collections = array();
		foreach( $metadata->associationMappings as $mapping ) {
			if( $mapping['type'] & ClassMetadataInfo::TO_MANY ) {
				$collections[] = '$this->' . $mapping['fieldName'] . ' = new \Doctrine\Common\Collections\ArrayCollection();';
			}
		}
		//XXX added
		$defaultValueString = $this->_generateDefaultValueInConstructor( $metadata );
		
		if( !$collections && '' == $defaultValueString ) {
			return '';
		}
		
		$currentCode = str_replace( "<defaultValues>", $defaultValueString, self::$_constructorMethodTemplate );
		
		if( $collections ) {
			$currentCode = str_replace( "<collections>", implode( "\n", $collections ), $currentCode );
		} else {
			$currentCode = str_replace( "<collections>", '', $currentCode );
		}
		
		return $this->_prefixCodeWithSpaces( $currentCode );
	}
	
	private function _generateDefaultValueInConstructor(ClassMetadataInfo $metadata) {
		$lines = array();
		foreach( $metadata->fieldMappings as $fieldMapping ) {
			if( !isset( $fieldMapping['default'] ) ) {
				continue;
			}
			$lines[] = $this->_spaces . '$this->' . $fieldMapping['fieldName'] . ' = ' . var_export( $fieldMapping['default'], true ) . ";";
		
		}
		
		return implode( "\n", $lines );
	}
	
	/**
	 * @todo this won't work if there is a namespace in brackets and a class outside of it.
	 * @param string $src
	 */
	private function _parseTokensInEntityFile($src) {
		$tokens = token_get_all( $src );
		$lastSeenNamespace = "";
		$lastSeenClass = false;
		
		$inNamespace = false;
		$inClass = false;
		for( $i = 0; $i < count( $tokens ); $i++ ) {
			$token = $tokens[$i];
			if( in_array( $token[0], array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ) ) ) {
				continue;
			}
			
			if( $inNamespace ) {
				if( $token[0] == T_NS_SEPARATOR || $token[0] == T_STRING ) {
					$lastSeenNamespace .= $token[1];
				} else if( is_string( $token ) && in_array( $token, array( ';', '{' ) ) ) {
					$inNamespace = false;
				}
			}
			
			if( $inClass ) {
				$inClass = false;
				$lastSeenClass = $lastSeenNamespace . '\\' . $token[1];
				$this->_staticReflection[$lastSeenClass]['properties'] = array();
				$this->_staticReflection[$lastSeenClass]['methods'] = array();
			}
			
			if( $token[0] == T_NAMESPACE ) {
				$lastSeenNamespace = "";
				$inNamespace = true;
			} else if( $token[0] == T_CLASS ) {
				$inClass = true;
			} else if( $token[0] == T_FUNCTION ) {
				if( $tokens[$i + 2][0] == T_STRING ) {
					$this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 2][1];
				} else if( $tokens[$i + 2][0] == T_AMPERSAND && $tokens[$i + 3][0] == T_STRING ) {
					$this->_staticReflection[$lastSeenClass]['methods'][] = $tokens[$i + 3][1];
				}
			} else if( in_array( $token[0], array( T_VAR, T_PUBLIC, T_PRIVATE, T_PROTECTED ) ) && $tokens[$i + 2][0] != T_FUNCTION ) {
				$this->_staticReflection[$lastSeenClass]['properties'][] = substr( $tokens[$i + 2][1], 1 );
			}
		}
	}
	
	private function _hasProperty($property, ClassMetadataInfo $metadata) {
		return (isset( $this->_staticReflection[$metadata->name] ) && in_array( $property, $this->_staticReflection[$metadata->name]['properties'] ));
	}
	
	private function _hasMethod($method, ClassMetadataInfo $metadata) {
		return (isset( $this->_staticReflection[$metadata->name] ) && in_array( $method, $this->_staticReflection[$metadata->name]['methods'] ));
	}
	
	private function _hasNamespace(ClassMetadataInfo $metadata) {
		return (isset( $metadata->namespace ) && $metadata->namespace) || (strpos( $metadata->name, '\\' ) ? true : false);
	}
	
	private function _extendsClass() {
		return $this->_classToExtend ? true : false;
	}
	
	private function _getClassToExtend() {
		return $this->_classToExtend;
	}
	
	private function _getClassToExtendName() {
		$refl = new \ReflectionClass( $this->_getClassToExtend() );
		
		return '\\' . $refl->getName();
	}
	
	private function _getClassName(ClassMetadataInfo $metadata) {
		return ($pos = strrpos( $metadata->name, '\\' )) ? substr( $metadata->name, $pos + 1, strlen( $metadata->name ) ) : $metadata->name;
	}
	
	//XXX add of the namespace support
	private function _getNamespace(ClassMetadataInfo $metadata) {
		if( isset( $metadata->namespace ) && $metadata->namespace ) {
			return $metadata->namespace;
		} else {
			return substr( $metadata->name, 0, strrpos( $metadata->name, '\\' ) );
		}
	}
	
	private function _generateEntityDocBlock(ClassMetadataInfo $metadata) {
		$lines = array();
		$lines[] = '/**';
		$lines[] = ' * ' . $metadata->name;
		
		if( $this->_generateAnnotations ) {
			$lines[] = ' *';
			
			$methods = array( '_generateTableAnnotation', '_generateInheritanceAnnotation', '_generateDiscriminatorColumnAnnotation', '_generateDiscriminatorMapAnnotation' );
			
			foreach( $methods as $method ) {
				$code = $this->$method( $metadata );
				if( $code ) {
					$lines[] = ' * ' . $code;
				}
			}
			
			if( $metadata->isMappedSuperclass ) {
				$lines[] = ' * @' . $this->_annotationsPrefix . 'MappedSuperClass';
			} else {
				$lines[] = ' * @' . $this->_annotationsPrefix . 'Entity';
			}
			
			if( $metadata->customRepositoryClassName ) {
				$lines[count( $lines ) - 1] .= '(repositoryClass="' . $metadata->customRepositoryClassName . '")';
			} else {
				$repository = $this->searchRepositoryFor( $metadata->name );
				if( $repository ) {
					$metadata->customRepositoryClassName = $repository;
					$lines[count( $lines ) - 1] .= '(repositoryClass="' . $metadata->customRepositoryClassName . '")';
				}
			}
			
			if( isset( $metadata->lifecycleCallbacks ) && $metadata->lifecycleCallbacks ) {
				$lines[] = ' * @' . $this->_annotationsPrefix . 'HasLifecycleCallbacks';
			}
		}
		
		$lines[] = ' */';
		
		return implode( "\n", $lines );
	}
	
	//XXX add of the repository support
	/** search in the directory that should contain the repositories and look for {$name}Repository class */
	private function searchRepositoryFor($name) {
		if( null === $this->_repositoryDirectory || !defined( 'TARGET' ) ) {
			return null;
		}
		if( '' === $this->_repositoryDirectory ) {
			$dirname = TARGET;
		} else {
			// for the moment, no need to support absolut path for repository
			$dirname = TARGET . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . $this->_repositoryDirectory;
		}
		$files = scandir( $dirname );
		foreach( $files as $file ) {
			$className = "{$name}Repository";
			if( $file === "{$className}.php" ) {
				// to test the inheritence, the problem is that we need to have the namespace, this is not absolutly necessary in our case
				//				$repo = $className();
				//				if( $repo && $repo instanceof EntityRepository ) {
				return "{$name}Repository";
			
		//				}
			}
		}
		return null;
	}
	
	private function _generateTableAnnotation(ClassMetadataInfo $metadata) {
		$table = array();
		if( $metadata->table['name'] ) {
			$table[] = 'name="' . $metadata->table['name'] . '"';
		}
		
		// indexes => array
		if( $metadata->table['indexes'] ) {
			// indexes={@Index(name="search_idx", columns={"name", "email"})}
			$indexes = array();
			foreach( $metadata->table['indexes'] as $key => $index ) {
				$columns = $index->getColumns();
				array_walk( $columns, function (&$item, $key) {
					$item = '"' . $item . '"';
				} );
				$indexes[] = " *\t\t@Index(name=\"" . $key . '", columns={' . implode( ', ', $columns ) . '})';
			}
			$table[] = "indexes={\n" . implode( ",\n", $indexes ) . "\n *\t}";
		}
		// uniqueConstraints => array
		if( $metadata->table['uniqueConstraints'] ) {
			// uniqueConstraints={@UniqueConstraint(name="search_idx", columns={"name", "email"})}
			$uniques = array();
			foreach( $metadata->table['uniqueConstraints'] as $key => $unique ) {
				$columns = $unique->getColumns();
				array_walk( $columns, function (&$item, $key) {
					$item = '"' . $item . '"';
				} );
				$uniques[] = " *\t\t@UniqueConstraint(name=\"" . $key . '", columns={' . implode( ', ', $columns ) . '})';
			}
			$table[] = "uniqueConstraints={\n" . implode( ",\n", $uniques ) . "\n *\t}";
		}
		
		return '@' . $this->_annotationsPrefix . "Table(\n *\t" . implode( ", \n *\t", $table ) . "\n * )";
	}
	
	private function _generateInheritanceAnnotation(ClassMetadataInfo $metadata) {
		if( $metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE ) {
			return '@' . $this->_annotationsPrefix . 'InheritanceType("' . $this->_getInheritanceTypeString( $metadata->inheritanceType ) . '")';
		}
	}
	
	private function _generateDiscriminatorColumnAnnotation(ClassMetadataInfo $metadata) {
		if( $metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE ) {
			$discrColumn = $metadata->discriminatorValue;
			$columnDefinition = 'name="' . $discrColumn['name'] . '", type="' . $discrColumn['type'] . '", length=' . $discrColumn['length'];
			
			return '@' . $this->_annotationsPrefix . 'DiscriminatorColumn(' . $columnDefinition . ')';
		}
	}
	
	private function _generateDiscriminatorMapAnnotation(ClassMetadataInfo $metadata) {
		if( $metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE ) {
			$inheritanceClassMap = array();
			
			foreach( $metadata->discriminatorMap as $type => $class ) {
				$inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"';
			}
			
			return '@' . $this->_annotationsPrefix . 'DiscriminatorMap({' . implode( ', ', $inheritanceClassMap ) . '})';
		}
	}
	
	private function _generateEntityStubMethods(ClassMetadataInfo $metadata) {
		$methods = array();
		
		foreach( $metadata->fieldMappings as $fieldMapping ) {
			if( !isset( $fieldMapping['id'] ) || !$fieldMapping['id'] || $metadata->generatorType == ClassMetadataInfo::GENERATOR_TYPE_NONE ) {
				$code = $this->_generateEntityStubMethod( $metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'] );
				if( $code ) {
					$methods[] = $code;
				}
			}
			$code = $this->_generateEntityStubMethod( $metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'] );
			if( $code ) {
				$methods[] = $code;
			}
		}
		
		foreach( $metadata->associationMappings as $associationMapping ) {
			if( $associationMapping['type'] & ClassMetadataInfo::TO_ONE ) {
				$code = $this->_generateEntityStubMethod( $metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'] );
				if( $code ) {
					$methods[] = $code;
				}
				$code = $this->_generateEntityStubMethod( $metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'] );
				if( $code ) {
					$methods[] = $code;
				}
			} else if( $associationMapping['type'] & ClassMetadataInfo::TO_MANY ) {
				$code = $this->_generateEntityStubMethod( $metadata, 'add', $associationMapping['fieldName'], $associationMapping['targetEntity'] );
				if( $code ) {
					$methods[] = $code;
				}
				$code = $this->_generateEntityStubMethod( $metadata, 'get', $associationMapping['fieldName'], 'Doctrine\Common\Collections\Collection' );
				if( $code ) {
					$methods[] = $code;
				}
			}
		}
		
		return implode( "\n\n", $methods );
	}
	
	private function _generateEntityLifecycleCallbackMethods(ClassMetadataInfo $metadata) {
		if( isset( $metadata->lifecycleCallbacks ) && $metadata->lifecycleCallbacks ) {
			$methods = array();
			
			foreach( $metadata->lifecycleCallbacks as $name => $callbacks ) {
				foreach( $callbacks as $callback ) {
					$code = $this->_generateLifecycleCallbackMethod( $name, $callback, $metadata );
					if( $code ) {
						$methods[] = $code;
					}
				}
			}
			
			return implode( "\n\n", $methods );
		}
		
		return "";
	}
	
	private function _generateEntityAssociationMappingProperties(ClassMetadataInfo $metadata) {
		$lines = array();
		
		foreach( $metadata->associationMappings as $associationMapping ) {
			if( $this->_hasProperty( $associationMapping['fieldName'], $metadata ) ) {
				continue;
			}
			
			$lines[] = $this->_generateAssociationMappingPropertyDocBlock( $associationMapping, $metadata );
			$lines[] = $this->_spaces . 'private $' . $associationMapping['fieldName'] . ($associationMapping['type'] == 'manyToMany' ? ' = array()' : null) . ";\n";
		}
		
		return implode( "\n", $lines );
	}
	
	private function _generateEntityFieldMappingProperties(ClassMetadataInfo $metadata) {
		$lines = array();
		
		foreach( $metadata->fieldMappings as $fieldMapping ) {
			if( $this->_hasProperty( $fieldMapping['fieldName'], $metadata ) || $metadata->isInheritedField( $fieldMapping['fieldName'] ) ) {
				continue;
			}
			
			$lines[] = $this->_generateFieldMappingPropertyDocBlock( $fieldMapping, $metadata );
			//XXX comment
			$lines[] = $this->_spaces . 'private $' . $fieldMapping['fieldName'] . ";\n";
		
		//			. (isset($fieldMapping['default']) ? ' = ' . var_export($fieldMapping['default'], true) : null) . ";\n";
		}
		
		return implode( "\n", $lines );
	}
	
	private function _generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null) {
		$methodName = $type . Inflector::classify( $fieldName );
		
		if( $this->_hasMethod( $methodName, $metadata ) ) {
			return;
		}
		
		$var = sprintf( '_%sMethodTemplate', $type );
		$template = self::$$var;
		
		$variableType = $typeHint ? $typeHint . ' ' : null;
		
		$types = \Doctrine\DBAL\Types\Type::getTypesMap();
		$methodTypeHint = $typeHint && !isset( $types[$typeHint] ) ? '\\' . $typeHint . ' ' : null;
		
		$replacements = array( '<description>' => ucfirst( $type ) . ' ' . $fieldName, '<methodTypeHint>' => $methodTypeHint, '<variableType>' => $variableType, '<variableName>' => Inflector::camelize( $fieldName ), '<methodName>' => $methodName, '<fieldName>' => $fieldName );
		
		$method = str_replace( array_keys( $replacements ), array_values( $replacements ), $template );
		
		return $this->_prefixCodeWithSpaces( $method );
	}
	
	private function _generateLifecycleCallbackMethod($name, $methodName, $metadata) {
		if( $this->_hasMethod( $methodName, $metadata ) ) {
			return;
		}
		
		$replacements = array( '<name>' => $this->_annotationsPrefix . $name, '<methodName>' => $methodName );
		
		$method = str_replace( array_keys( $replacements ), array_values( $replacements ), self::$_lifecycleCallbackMethodTemplate );
		
		return $this->_prefixCodeWithSpaces( $method );
	}
	
	private function _generateJoinColumnAnnotation(array $joinColumn) {
		$joinColumnAnnot = array();
		
		if( isset( $joinColumn['name'] ) ) {
			$joinColumnAnnot[] = 'name="' . $joinColumn['name'] . '"';
		}
		
		if( isset( $joinColumn['referencedColumnName'] ) ) {
			$joinColumnAnnot[] = 'referencedColumnName="' . $joinColumn['referencedColumnName'] . '"';
		}
		
		if( isset( $joinColumn['unique'] ) && $joinColumn['unique'] ) {
			$joinColumnAnnot[] = 'unique=' . ($joinColumn['unique'] ? 'true' : 'false');
		}
		
		if( isset( $joinColumn['nullable'] ) ) {
			$joinColumnAnnot[] = 'nullable=' . ($joinColumn['nullable'] ? 'true' : 'false');
		}
		
		if( isset( $joinColumn['onDelete'] ) ) {
			$joinColumnAnnot[] = 'onDelete=' . ($joinColumn['onDelete'] ? 'true' : 'false');
		}
		
		if( isset( $joinColumn['onUpdate'] ) ) {
			$joinColumnAnnot[] = 'onUpdate=' . ($joinColumn['onUpdate'] ? 'true' : 'false');
		}
		
		if( isset( $joinColumn['columnDefinition'] ) ) {
			$joinColumnAnnot[] = 'columnDefinition="' . $joinColumn['columnDefinition'] . '"';
		}
		
		return '@' . $this->_annotationsPrefix . 'JoinColumn(' . implode( ', ', $joinColumnAnnot ) . ')';
	}
	
	private function _generateAssociationMappingPropertyDocBlock(array $associationMapping, ClassMetadataInfo $metadata) {
		$lines = array();
		$lines[] = $this->_spaces . '/**';
		$lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity'];
		
		if( $this->_generateAnnotations ) {
			$lines[] = $this->_spaces . ' *';
			
			$type = null;
			switch ( $associationMapping['type'] ) {
				case ClassMetadataInfo::ONE_TO_ONE :
					$type = 'OneToOne';
					break;
				case ClassMetadataInfo::MANY_TO_ONE :
					$type = 'ManyToOne';
					break;
				case ClassMetadataInfo::ONE_TO_MANY :
					$type = 'OneToMany';
					break;
				case ClassMetadataInfo::MANY_TO_MANY :
					$type = 'ManyToMany';
					break;
			}
			$typeOptions = array();
			
			if( isset( $associationMapping['targetEntity'] ) ) {
				$typeOptions[] = 'targetEntity="' . $associationMapping['targetEntity'] . '"';
			}
			
			if( isset( $associationMapping['inversedBy'] ) ) {
				$typeOptions[] = 'inversedBy="' . $associationMapping['inversedBy'] . '"';
			}
			
			if( isset( $associationMapping['mappedBy'] ) ) {
				$typeOptions[] = 'mappedBy="' . $associationMapping['mappedBy'] . '"';
			}
			
			if( $associationMapping['cascade'] ) {
				$cascades = array();
				
				if( $associationMapping['isCascadePersist'] )
					$cascades[] = '"persist"';
				if( $associationMapping['isCascadeRemove'] )
					$cascades[] = '"remove"';
				if( $associationMapping['isCascadeDetach'] )
					$cascades[] = '"detach"';
				if( $associationMapping['isCascadeMerge'] )
					$cascades[] = '"merge"';
				if( $associationMapping['isCascadeRefresh'] )
					$cascades[] = '"refresh"';
				
				$typeOptions[] = 'cascade={' . implode( ',', $cascades ) . '}';
			}
			
			if( isset( $associationMapping['orphanRemoval'] ) && $associationMapping['orphanRemoval'] ) {
				$typeOptions[] = 'orphanRemoval=' . ($associationMapping['orphanRemoval'] ? 'true' : 'false');
			}
			
			$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . '' . $type . '(' . implode( ', ', $typeOptions ) . ')';
			
			if( isset( $associationMapping['joinColumns'] ) && $associationMapping['joinColumns'] ) {
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'JoinColumns({';
				
				$joinColumnsLines = array();
				
				foreach( $associationMapping['joinColumns'] as $joinColumn ) {
					$joinColumnAnnot = $this->_generateJoinColumnAnnotation( $joinColumn );
					if( $joinColumnAnnot ) {
						$joinColumnsLines[] = $this->_spaces . ' *   ' . $joinColumnAnnot;
					}
				}
				
				$lines[] = implode( ",\n", $joinColumnsLines );
				$lines[] = $this->_spaces . ' * })';
			}
			
			if( isset( $associationMapping['joinTable'] ) && $associationMapping['joinTable'] ) {
				$joinTable = array();
				$joinTable[] = 'name="' . $associationMapping['joinTable']['name'] . '"';
				
				if( isset( $associationMapping['joinTable']['schema'] ) ) {
					$joinTable[] = 'schema="' . $associationMapping['joinTable']['schema'] . '"';
				}
				
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'JoinTable(' . implode( ', ', $joinTable ) . ',';
				$lines[] = $this->_spaces . ' *   joinColumns={';
				
				foreach( $associationMapping['joinTable']['joinColumns'] as $joinColumn ) {
					$lines[] = $this->_spaces . ' *     ' . $this->_generateJoinColumnAnnotation( $joinColumn );
				}
				
				$lines[] = $this->_spaces . ' *   },';
				$lines[] = $this->_spaces . ' *   inverseJoinColumns={';
				
				foreach( $associationMapping['joinTable']['inverseJoinColumns'] as $joinColumn ) {
					$lines[] = $this->_spaces . ' *     ' . $this->_generateJoinColumnAnnotation( $joinColumn );
				}
				
				$lines[] = $this->_spaces . ' *   }';
				$lines[] = $this->_spaces . ' * )';
			}
			
			if( isset( $associationMapping['orderBy'] ) ) {
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'OrderBy({';
				
				foreach( $associationMapping['orderBy'] as $name => $direction ) {
					$lines[] = $this->_spaces . ' *     "' . $name . '"="' . $direction . '",';
				}
				
				$lines[count( $lines ) - 1] = substr( $lines[count( $lines ) - 1], 0, strlen( $lines[count( $lines ) - 1] ) - 1 );
				$lines[] = $this->_spaces . ' * })';
			}
		}
		
		$lines[] = $this->_spaces . ' */';
		
		return implode( "\n", $lines );
	}
	
	private function _generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata) {
		$lines = array();
		$lines[] = $this->_spaces . '/**';
		$lines[] = $this->_spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName'];
		
		if( $this->_generateAnnotations ) {
			$lines[] = $this->_spaces . ' *';
			
			$column = array();
			if( isset( $fieldMapping['columnName'] ) ) {
				$column[] = 'name="' . $fieldMapping['columnName'] . '"';
			}
			
			if( isset( $fieldMapping['type'] ) ) {
				$column[] = 'type="' . $fieldMapping['type'] . '"';
			}
			
			if( isset( $fieldMapping['length'] ) ) {
				$column[] = 'length=' . $fieldMapping['length'];
			}
			
			if( isset( $fieldMapping['precision'] ) ) {
				$column[] = 'precision=' . $fieldMapping['precision'];
			}
			
			if( isset( $fieldMapping['scale'] ) ) {
				$column[] = 'scale=' . $fieldMapping['scale'];
			}
			
			if( isset( $fieldMapping['nullable'] ) ) {
				$column[] = 'nullable=' . var_export( $fieldMapping['nullable'], true );
			}
			
			if( isset( $fieldMapping['columnDefinition'] ) ) {
				$column[] = 'columnDefinition="' . $fieldMapping['columnDefinition'] . '"';
			}
			
			// need to check if the unique/index are alone, otherwise that leads to errors after 1 loop
			if( isset( $metadata->table['indexes'] ) ) {
				$searchKey = $fieldMapping['columnName'];
				$result = array_filter( $metadata->table['indexes'], function (Index $index) use($searchKey) {
					$cols = $index->getColumns();
					if( 1 == count( $cols ) ) {
						if( $searchKey === $cols[0] ) {
							return true;
						}
					}
					return false;
				} );
				if( $result ) {
					$column[] = 'index=' . var_export( $fieldMapping['index'], true );
				}
			
			}
			if( isset( $metadata->table['uniqueConstraints'] ) ) {
				$searchKey = $fieldMapping['columnName'];
				$result = array_filter( $metadata->table['uniqueConstraints'], function (Index $index) use($searchKey) {
					$cols = $index->getColumns();
					if( 1 == count( $cols ) ) {
						if( $searchKey === $cols[0] ) {
							return true;
						}
					}
					return false;
				} );
				if( $result ) {
					$column[] = 'unique=' . var_export( $fieldMapping['unique'], true );
				}
			
			}
			//			if( isset( $fieldMapping['unique'] ) && $fieldMapping['unique'] ) {
			//				$column[] = 'unique=' . var_export( $fieldMapping['unique'], true );
			//			}
			//			
			//			if( isset( $fieldMapping['index'] ) && $fieldMapping['index'] ) {
			//				$column[] = 'index=' . var_export( $fieldMapping['index'], true );
			//			}
			

			//XXX add
			if( isset( $fieldMapping['default'] ) ) {
				if( is_string( $fieldMapping['default'] ) ) {
					$column[] = 'default="' . $fieldMapping['default'] . '"';
				} else if( is_numeric( $fieldMapping['default'] ) ) {
					$column[] = 'default=' . $fieldMapping['default'];
				}
			}
			
			$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'Column(' . implode( ', ', $column ) . ')';
			
			// XXX add comment support
			if( isset( $fieldMapping['comment'] ) && $fieldMapping['comment'] ) {
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'Comment("' . $fieldMapping['comment'] . '")';
			}
			
			if( isset( $fieldMapping['id'] ) && $fieldMapping['id'] ) {
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'Id';
				$generatorType = $this->_getIdGeneratorTypeString( $metadata->generatorType );
				if( $generatorType ) {
					$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'GeneratedValue(strategy="' . $generatorType . '")';
				}
				
				if( $metadata->sequenceGeneratorDefinition ) {
					$sequenceGenerator = array();
					
					if( isset( $metadata->sequenceGeneratorDefinition['sequenceName'] ) ) {
						$sequenceGenerator[] = 'sequenceName="' . $metadata->sequenceGeneratorDefinition['sequenceName'] . '"';
					}
					
					if( isset( $metadata->sequenceGeneratorDefinition['allocationSize'] ) ) {
						$sequenceGenerator[] = 'allocationSize="' . $metadata->sequenceGeneratorDefinition['allocationSize'] . '"';
					}
					
					if( isset( $metadata->sequenceGeneratorDefinition['initialValue'] ) ) {
						$sequenceGenerator[] = 'initialValue="' . $metadata->sequenceGeneratorDefinition['initialValue'] . '"';
					}
					
					$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'SequenceGenerator(' . implode( ', ', $sequenceGenerator ) . ')';
				}
			}
			
			if( isset( $fieldMapping['version'] ) && $fieldMapping['version'] ) {
				$lines[] = $this->_spaces . ' * @' . $this->_annotationsPrefix . 'Version';
			}
		}
		
		$lines[] = $this->_spaces . ' */';
		
		return implode( "\n", $lines );
	}
	
	private function _prefixCodeWithSpaces($code, $num = 1) {
		$lines = explode( "\n", $code );
		
		foreach( $lines as $key => $value ) {
			$lines[$key] = str_repeat( $this->_spaces, $num ) . $lines[$key];
		}
		
		return implode( "\n", $lines );
	}
	
	private function _getInheritanceTypeString($type) {
		switch ( $type ) {
			case ClassMetadataInfo::INHERITANCE_TYPE_NONE :
				return 'NONE';
			
			case ClassMetadataInfo::INHERITANCE_TYPE_JOINED :
				return 'JOINED';
			
			case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE :
				return 'SINGLE_TABLE';
			
			case ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS :
				return 'PER_CLASS';
			
			default :
				throw new \InvalidArgumentException( 'Invalid provided InheritanceType: ' . $type );
		}
	}
	
	private function _getChangeTrackingPolicyString($policy) {
		switch ( $policy ) {
			case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT :
				return 'DEFERRED_IMPLICIT';
			
			case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT :
				return 'DEFERRED_EXPLICIT';
			
			case ClassMetadataInfo::CHANGETRACKING_NOTIFY :
				return 'NOTIFY';
			
			default :
				throw new \InvalidArgumentException( 'Invalid provided ChangeTrackingPolicy: ' . $policy );
		}
	}
	
	private function _getIdGeneratorTypeString($type) {
		switch ( $type ) {
			case ClassMetadataInfo::GENERATOR_TYPE_AUTO :
				return 'AUTO';
			
			case ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE :
				return 'SEQUENCE';
			
			case ClassMetadataInfo::GENERATOR_TYPE_TABLE :
				return 'TABLE';
			
			case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY :
				return 'IDENTITY';
			
			case ClassMetadataInfo::GENERATOR_TYPE_NONE :
				return 'NONE';
			
			default :
				throw new \InvalidArgumentException( 'Invalid provided IdGeneratorType: ' . $type );
		}
	}
}